import { Inject } from '@nestjs/common';
import { ENABLE_INSTRUMENTATION_ENV_VAR, PluginCommonModule, VendurePlugin } from '@vendure/core';

import { defaultMethodHooks } from './config/default-method-hooks';
import { OtelInstrumentationStrategy } from './config/otel-instrumentation-strategy';
import { OtelLogger } from './config/otel-logger';
import { TELEMETRY_PLUGIN_OPTIONS } from './constants';
import { MethodHooksService } from './service/method-hooks.service';
import { TelemetryPluginOptions } from './types';

/**
 * @description
 * The TelemetryPlugin is used to instrument the Vendure application and collect telemetry data using
 * [OpenTelemetry](https://opentelemetry.io/).
 *
 * ## Installation
 *
 * ```
 * npm install \@vendure/telemetry-plugin
 * ```
 *
 * :::info
 * For a complete guide to setting up and working with Open Telemetry, see
 * the [Implementing Open Telemetry guide](/guides/how-to/telemetry/).
 * :::
 *
 * ## Configuration
 *
 * The plugin is configured via the `TelemetryPlugin.init()` method. This method accepts an options object
 * which defines the OtelLogger options and method hooks.
 *
 * @example
 * ```ts
 * import { VendureConfig } from '\@vendure/core';
 * import { TelemetryPlugin, registerMethodHooks } from '\@vendure/telemetry-plugin';
 *
 * export const config: VendureConfig = {
 *   // ...
 *   plugins: [
 *     TelemetryPlugin.init({
 *       loggerOptions: {
 *         // Log to the console at the verbose level
 *         console: LogLevel.Verbose,
 *       },
 *     }),
 *   ],
 * };
 * ```
 *
 * ## Preloading the SDK
 *
 * In order to use the OpenTelemetry SDK, you must preload it before the Vendure server is started.
 * This is done by using the `--require` flag when starting the server with a custom preload script.
 *
 * Create a file named `instrumentation.ts` in the root of your project and add the following code:
 *
 * ```ts
 * import { OTLPLogExporter } from '\@opentelemetry/exporter-logs-otlp-proto';
 * import { OTLPTraceExporter } from '\@opentelemetry/exporter-trace-otlp-http';
 * import { BatchLogRecordProcessor } from '\@opentelemetry/sdk-logs';
 * import { NodeSDK } from '\@opentelemetry/sdk-node';
 * import { BatchSpanProcessor } from '\@opentelemetry/sdk-trace-base';
 * import { getSdkConfiguration } from '\@vendure/telemetry-plugin/preload';
 *
 * // In this example we are using Loki as the OTLP endpoint for logging
 * process.env.OTEL_EXPORTER_OTLP_ENDPOINT = 'http://localhost:3100/otlp';
 * process.env.OTEL_LOGS_EXPORTER = 'otlp';
 * process.env.OTEL_RESOURCE_ATTRIBUTES = 'service.name=vendure-dev-server';
 *
 * // We are using Jaeger as the OTLP endpoint for tracing
 * const traceExporter = new OTLPTraceExporter({
 *     url: 'http://localhost:4318/v1/traces',
 * });
 * const logExporter = new OTLPLogExporter();
 *
 * // The getSdkConfiguration method returns a configuration object for the OpenTelemetry Node SDK.
 * // It also performs other configuration tasks such as setting a special environment variable
 * // to enable instrumentation in the Vendure core code.
 * const config = getSdkConfiguration({
 *     config: {
 *         // Pass in any custom configuration options for the Node SDK here
 *         spanProcessors: [new BatchSpanProcessor(traceExporter)],
 *         logRecordProcessors: [new BatchLogRecordProcessor(logExporter)],
 *     },
 * });
 *
 * const sdk = new NodeSDK(config);
 *
 * sdk.start();
 * ```
 *
 * The server would then be started with the following command:
 *
 * ```bash
 * node --require ./dist/instrumentation.js ./dist/server.js
 * ```
 *
 * or for development with ts-node:
 *
 * ```bash
 * npx ts-node --require ./src/instrumentation.ts ./src/server.ts
 * ```
 *
 * @since 3.3.0
 * @docsCategory core plugins/TelemetryPlugin
 */
@VendurePlugin({
    imports: [PluginCommonModule],
    providers: [
        MethodHooksService,
        {
            provide: TELEMETRY_PLUGIN_OPTIONS,
            useFactory: () => TelemetryPlugin.options,
        },
    ],
    configuration: config => {
        config.systemOptions.instrumentationStrategy = new OtelInstrumentationStrategy();
        config.logger = new OtelLogger(TelemetryPlugin.options.loggerOptions ?? {});
        return config;
    },
    compatibility: '>=3.3.0',
})
export class TelemetryPlugin {
    static options: TelemetryPluginOptions = {};

    constructor(
        methodHooksService: MethodHooksService,
        @Inject(TELEMETRY_PLUGIN_OPTIONS) options: TelemetryPluginOptions,
    ) {
        if (process.env[ENABLE_INSTRUMENTATION_ENV_VAR]) {
            const allMethodHooks = [...defaultMethodHooks, ...(options.methodHooks ?? [])];
            for (const methodHook of allMethodHooks) {
                methodHooksService.registerHooks(methodHook.target, methodHook.hooks);
            }
        }
    }

    static init(options: TelemetryPluginOptions) {
        TelemetryPlugin.options = options;
        return TelemetryPlugin;
    }
}
